When writing code during the development of software applications, developers commonly spend a significant amount of time “debugging” the code to find runtime and other source code errors. In doing so, developers may take several approaches to reproduce and localize a source code bug, such as observing behavior of a program based on different inputs, inserting debugging code (e.g., to print variable values, to track branches of execution, etc.), temporarily removing code portions, etc. Tracking down runtime errors to pinpoint code bugs can occupy a significant portion of application development time.
Many types of debugging applications (“debuggers”) have been developed in order to assist developers with the code debugging process. These tools offer developers the ability to trace, visualize, and alter the execution of computer code. For example, debuggers may visualize the execution of code instructions, may present code variable values at various times during code execution, may enable developers to alter code execution paths, and/or may enable developers to set “breakpoints” and/or “watchpoints” on code elements of interest (which, when reached during execution, causes execution of the code to be suspended), among other things.
An emerging form of debugging applications enable “time travel,” “reverse,” or “historic” debugging. With “time travel” debugging, execution of a program (e.g., executable entities such as threads) is recorded/traced by a trace application into one or more trace files. These trace file(s) can then be used to replay execution of the program later, for both forward and backward analysis. For example, “time travel” debuggers can enable a developer to set forward breakpoints/watchpoints (like conventional debuggers) as well as reverse breakpoints/watchpoints.
One challenge in implementing “time travel” debuggers involves how to return a memory value at a given point in a trace. One approach is to record a full copy of addressable memory of an executable entity at trace time, often using the same memory addresses that were seen by the executable entity at trace time. Then, replay involves loading this full memory copy, and performing memory reads and writes directly into it. Using this approach, reproducing memory values at a particular point involves reading the value from the memory copy. However, this approach produces prohibitively large amount of data at trace time (since a full copy of addressable memory is stored), and requires this large amount of data to be loaded at replay time. Furthermore, requiring maintenance of a large memory copy at replay time makes it impractical to perform parallel or speculative replay operations, and to store the values of memory at multiple particular points in time (for later retrieval).
Another approach is to replay code up to the last point that a memory address of interest was consumed prior to the point of interest in the trace, and return the value seen at that address. This has an advantage over the previous approach of using much less memory at replay time, but it introduces complexity and leads to poor performance for returning memory values, since a replay is performed in response to every request for a memory value.